<?php
/*
 *  测试usdt代币：0x337610d27c682E347C9cD60BD4b3b107C9d34dDd
 *
 * */
use Elliptic\EC;
use Ethereum\Eth;
use Binance\Formatter;
use Ethereum\HecoApi;
use Ethereum\HRC20;
use Ethereum\PEMHelper;
use Binance\Utils;
use Ethereum\TransactionEvent;
use kornrunner\Keccak;
use Web3p\EthereumTx\Transaction;

class Bsc
{
    const HECO_KEY = '';
    const WALLET_PRIVATE_KEY = '';
    const WALLET_ADDRESS = '';

    const SUCCESS_TX_HASH = '';
    const FAIL_TX_HASH = '';
    public $api_url; // 公共节点

    public $chain_id; // 节点id

    public $contract_address; // 代币合约地址

    public $api_key; // 密钥

    public $redis;

    public $bnb_decimal = 18;

    public $bnb20_decimal;

    public function __construct($contract_address = '')
    {
        $this->chain_id = 56;
        if(!$contract_address){
            $this->contract_address = '0x55d398326f99059ff775485246999027b3197955'; // USDT地址
        }else{
            $this->contract_address = $contract_address;
        }
        $this->redis = new \think\cache\driver\Redis(['select' => 1]);
        $this->bnb20_decimal = $this->contractDecimal($this->contract_address);
    }

    /*
     *  公共节点池
     * */
    public function apiUrlList(){
        $api_url_list = [
            'https://bsc-dataseed1.ninicoin.io/',
            'https://bsc-dataseed2.ninicoin.io/',
            'https://bsc-dataseed3.ninicoin.io/',
            'https://bsc-dataseed4.ninicoin.io/',
            'https://bsc-dataseed1.defibit.io/',
            'https://bsc-dataseed2.defibit.io/',
            'https://bsc-dataseed3.defibit.io/',
            'https://bsc-dataseed4.defibit.io/',
        ];
        return $api_url_list;
    }

    /*
     *  获取节点链接
     * */
    public function getNodeUrl(){
        $api_url_list = $this->apiUrlList();
        $redis_key = 'public_rpc_node';
        $n = $this->redis->inc($redis_key,1);
        $node_num = count($api_url_list);
        $k = $n%$node_num;
        return $api_url_list[$k] ?? '';
    }

    /*
    *  获取币安余额
    * */
    public function balance($address)
    {
        $params = [$address, 'latest'];
        $res = $this->request("eth_getBalance", $params);
        if(!empty($res['data']['result'])){
            $balance = Utils::toDisplayAmount($res['data']['result'], $this->bnb_decimal);
        }
        return $balance ?? 0;
    }

    /*
    *  获取代币余额
    *  @param $contract_address 代币合约地址
    *  @param $address 要查询的钱包地址
    * */
    public function contractBalance($address)
    {
        $formatMethod = Formatter::toMethodFormat('balanceOf(address)');
        $formatAddress = Formatter::toAddressFormat($address);
        $data_arr = [
            'from' => $address,
            'to' => $this->contract_address,
            'data' => "0x{$formatMethod}{$formatAddress}"
        ];
        $params = [$data_arr, 'latest'];
        $res = $this->request("eth_call", $params); //eth_call 用来调用合约函数
        if(!empty($res['data']['result'])) {
            $balance = Utils::toDisplayAmount($res['data']['result'], $this->bnb20_decimal);
        }
        return $balance ?? 0;
    }

    /*
     *  获取代币精度
     *  @param $contract_address 代币合约地址
     * */
    public function contractDecimal($contract_address)
    {
        try{
            $redis_key = md5('contractDecimal_'.$contract_address);
            $decimals = $this->redis->get($redis_key);
            if($decimals) return $decimals;
            $formatMethod = Formatter::toMethodFormat('decimals()');
            $data_arr = [
                'to' => $contract_address,
                'data' => "0x{$formatMethod}"
            ];
            $params = [$data_arr, 'latest'];
            $res = $this->request("eth_call", $params); //eth_call 用来调用合约函数
            if(!empty($res['data']['result'])){
                $decimals = Utils::toDisplayAmount($res['data']['result'],0);
                if($decimals > 0){
                    $this->redis->set($redis_key,$decimals);
                }
            }
            return $decimals ?? 0;
        }catch (\Exception $e){
            return 18;
        }
    }

    /*
     *  根据交易哈希查询交易状态
     *  http://cw.hubwiz.com/card/c/ethereum-json-rpc-api/1/3/26/
     *  @param $txid 交易哈希
     *  @param $tanfer_type 要查询的转账类型(2--转入；0--转出)
     * */
    public function receiptStatusByTxid($txid, $tanfer_type = 2)
    {
        $res = $this->getTransactionReceiptByTxid($txid);
        if($res['code'] == 1002){
            returnData('数据获取失败',1002); // 请求失败稍后再请求
        }
        if($res['code'] != 1 || empty($res['data']['result'])){
            returnData('数据获取失败'); // 请求失败稍后再请求
        }
        // status(1--成功；0--失败) type(2--转入；0--转出)
        $status = Utils::toDisplayAmount($res['data']['result']['status'],0);
        $now_tanfer_type = Utils::toDisplayAmount($res['data']['result']['type'],0);
        if($status != 1 || $tanfer_type != $now_tanfer_type){
            return returnData('转账失败');
        }
        return returnData('success',1);
    }

    /*
     *  根据交易hash获取交易详情
     * */
    public function getTransactionReceiptByTxid($txid){
        return $this->request("eth_getTransactionReceipt", [$txid]); //eth_call 用来调用合约函数
    }


    /*
     *  代币转账
     *  @param $to_adderss 收款钱包地址
     *  @param $from_address 转账钱包地址
     *  @param $privateKey 钱包密钥
     *  @param $transfer_num 转账数量
     * */
    public function contractTranfer($from_address, $transfer_num, $privateKey, $to_adderss){

        $params = [
            'from' => $from_address,
            'to' => $this->contract_address,
            'value' => Utils::NONE,
        ];
        $transfer_num = Utils::toMinUnitByDecimals("$transfer_num", $this->bnb20_decimal);
        $formatInteger = Formatter::toIntegerFormat($transfer_num);
        $formatMethod = Formatter::toMethodFormat('transfer(address,uint256)');
        $formatAddress = Formatter::toAddressFormat($to_adderss);
        $params['data'] = "0x{$formatMethod}{$formatAddress}{$formatInteger}";
        $params['gas'] = $this->getGas($params);
        $params['nonce'] = $this->getNonce($from_address);
        $params['gasPrice'] = $this->getGasPrice();
        return $this->sendRawTransaction($params, $privateKey);
    }

    /*
     *  为签名交易创建一个新的消息调用交易或合约。
     *  @param $data string 签名的交易数据
     *  return txid 交易hash
     * */
    public function sendRawTransaction($transaction, $privateKey){
        // 合并数据
        $transaction = array_merge([
            'nonce' => '',
            'gasPrice' => '',
            'gas' => '',
            'to' => '',
            'value' => '',
            'data' => '',
            'chainId' => $this->chain_id,
        ], $transaction);
        $transaction = new Transaction($transaction);
        $raw = $transaction->sign($privateKey);
        $res = $this->request("eth_sendRawTransaction", ['0x'.$raw]);
        if($res['code'] == 1){
            return returnData('success',1,$res['data']['result'] ?? '');
        }else{
            return returnData($res['msg'],$res['code']);
        }
    }

    /*
    *  @api:http://cw.hubwiz.com/card/c/ethereum-json-rpc-api/1/3/6/
    *  当前的gas价格，单位：wei。
     * return 十六进制
    * */
    public function getGas($data){
        $params = [$data];
        $res = $this->request("eth_estimateGas", $params);
        if(!empty($res['data']['result'])){
//            $gas = Utils::toDisplayAmount($res['data']['result'],$this->bnb20_decimal);
            $gas = $res['data']['result'];
        }
        return $gas ?? '';
    }

    /*
    *  @api:http://cw.hubwiz.com/card/c/ethereum-json-rpc-api/1/3/6/
    *  当前的gas价格，单位：wei。
     * return 十六进制
    * */
    public function getGasPrice(){
        $res = $this->request("eth_gasPrice");
        if(empty($res['data']['result'])){
            return '';
        }
        return $res['data']['result'];
    }

    /*
     *  $address 打款钱包
     *  return 十六进制
     * */
    public function getNonce($address){
        $params = [$address, 'latest'];
        $res = $this->request("eth_getTransactionCount", $params);
        return $res['data']['result'] ?? '';
    }

    /*
     *  curl 请求
     * */
    private function request($methond, $params = [])
    {
        $api_url = $this->getNodeUrl();
        if(!$api_url) return returnData('节点链接不能为空');
        $send_data = [
            'jsonrpc' => '2.0',
            'method' => $methond,
            'id' => $this->chain_id,
            'params' => $params
        ];
        $curl = curl_init();
        curl_setopt_array($curl, array(
            CURLOPT_URL => $api_url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
//            CURLOPT_USERAGENT => "Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20100101 Firefox/12.0",
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_POSTFIELDS => json_encode($send_data),
            CURLOPT_HTTPHEADER => array(
                'Content-Type: application/json'
            ),
        ));
        $response = curl_exec($curl);
        $err = curl_error($curl);
        $http_status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        if($http_status != 200 || $err){
            return returnData('请求被拒',1002);
        }
        curl_close($curl);
        $response = json_decode(trim($response),1);
        if($response){
            return returnData('success',1, $response);
        }
        return returnData('请求失败');
    }

    public function verify_sign(string $message, string $signature, string $address) : bool
    {
        $hash = Keccak::hash(sprintf("\x19Ethereum Signed Message:\n%s%s", strlen($message), $message), 256);
        $sign   = ['r' => substr($signature, 2, 64), 's' => substr($signature, 66, 64)];
        $recid  = ord(hex2bin(substr($signature, 130, 2))) - 27;
        if ($recid != ($recid & 1)) {
            return false;
        }
        $pubkey = (new EC('secp256k1'))->recoverPubKey($hash, $sign, $recid);
        $pubAddress = substr(Keccak::hash(substr(hex2bin($pubkey->encode('hex')), 1), 256), 24);
        return strtolower("0x".$pubAddress) === strtolower($address);
    }


}

?>